// 《围棋》作者版权所有。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

// go:build aix | | |（solaris&&！illumos）

// 这段代码使用POSIX'fcntl'锁实现filelock API，它将
// 附加到（inode，process）对上，而不是文件描述符上。为了避免在同一个文件通过不同的描述符打开时过早解锁
// 文件，
// 我们一次只允许一个读锁。
// 
// 大多数平台都提供了一些可选的API，例如'flock'系统调用
// 或'fcntl'的F_of d_SETLK命令，它允许更好的并发性，并且
// 不需要在应用程序中按索引节点记账。

package filelock

import (
	"errors"
	"io"
	"io/fs"
	"math/rand"
	"sync"
	"syscall"
	"time"
)

type lockType int16

const (
	readLock  lockType = syscall.F_RDLCK
	writeLock lockType = syscall.F_WRLCK
)

type inode = uint64 // 系统调用的类型。Stat_t.Ino 

type inodeLock struct {
	owner File
	queue []<-chan File
}

type token struct{}

var (
	mu     sync.Mutex
	inodes = map[File]inode{}
	locks  = map[inode]inodeLock{}
)

func lock(f File, lt lockType) (err error) {
	// POSIX锁适用于每个inode和进程，当该inode的*any*描述符关闭时，inode的锁被释放。所以我们需要在内部同步对每个inode的访问，并且必须序列化lock和解锁调用，通过不同的描述符引用同一个inode。
	fi, err := f.Stat()
	if err != nil {
		return err
	}
	ino := fi.Sys().(*syscall.Stat_t).Ino

	mu.Lock()
	if i, dup := inodes[f]; dup && i != ino {
		mu.Unlock()
		return &fs.PathError{
			Op:   lt.String(),
			Path: f.Name(),
			Err:  errors.New("inode for file changed since last Lock or RLock"),
		}
	}
	inodes[f] = ino

	var wait chan File
	l := locks[ino]
	if l.owner == f {
		// 此文件已拥有锁，但调用可能会更改其锁类型。
	} else if l.owner == nil {
		// 没有所有者：它现在是我们的了。
		l.owner = f
	} else {
		// 已拥有：添加频道等待。
		wait = make(chan File)
		l.queue = append(l.queue, wait)
	}
	locks[ino] = l
	mu.Unlock()

	if wait != nil {
		wait <- f
	}

	// 在以
	// 进程而非线程级别计算死锁图的平台上，会出现虚假的EDEADLK错误。考虑进程P和Q，用ABCFDG 
	// P.1解锁文件A.
	// Q.3解锁和锁定文件A.
	// Q.3解锁文件A和B.
	// P.2解锁和锁定文件B.
	// P.2解锁文件B.
	// 
	// 
	// 
	// 我们通过将EDEADLK视为始终虚假来解决此错误。如果交互进程之间真的存在锁排序错误，它将变成一个活锁，但这并不比我们有一个合适的flock实现（通常不会尝试诊断死锁）更糟糕。在上面的示例中，这将跟踪更改为：
	// 
	// P.1锁定文件A.
	// Q.3锁定文件B.
	// Q.3阻止文件A.
	// P.2错误地未能锁定文件B并进入睡眠状态。
	// P.1解锁文件A.
	// Q.3解锁和锁定文件A.
	// Q.3解锁文件A和B.
	// P.2唤醒并锁定文件B.
	// P.2解锁文件B.
	// 
	// 我们知道重试循环不会引入*伪*活锁
	// ，根据POSIX规范，只有当“锁被另一个进程的锁阻塞”时，才会返回EDEADLK。
	// 如果该进程在我们持有的某个锁上被阻止，那么产生的livelock是由于一个真正的死锁造成的（例如，在使用这个包的flock实现时，它会表现为这样的
	// ）。
	// 如果另一个进程在我们持有的另一个锁上*未*被阻止，那么它最终将释放请求的锁。

	nextSleep := 1 * time.Millisecond
	const maxSleep = 500 * time.Millisecond
	for {
		err = setlkw(f.Fd(), lt)
		if err != syscall.EDEADLK {
			break
		}
		time.Sleep(nextSleep)

		nextSleep += nextSleep
		if nextSleep > maxSleep {
			nextSleep = maxSleep
		}
		// 当我们最终解锁时，应用10%的抖动以避免同步冲突。
		nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
	}

	if err != nil {
		unlock(f)
		return &fs.PathError{
			Op:   lt.String(),
			Path: f.Name(),
			Err:  err,
		}
	}

	return nil
}

func unlock(f File) error {
	var owner File

	mu.Lock()
	ino, ok := inodes[f]
	if ok {
		owner = locks[ino].owner
	}
	mu.Unlock()

	if owner != f {
		panic("unlock called on a file that is not locked")
	}

	err := setlkw(f.Fd(), syscall.F_UNLCK)

	mu.Lock()
	l := locks[ino]
	if len(l.queue) == 0 {
		// 无服务员：删除地图条目。
		delete(locks, ino)
	} else {
		// 第一个服务员正在把他们的档案寄给我们。
		// 接收并更新队列。
		l.owner = <-l.queue[0]
		l.queue = l.queue[1:]
		locks[ino] = l
	}
	delete(inodes, f)
	mu.Unlock()

	return err
}

// setlkw使用F_setlkw为fd指示的整个文件调用FcntlFlock。
func setlkw(fd uintptr, lt lockType) error {
	for {
		err := syscall.FcntlFlock(fd, syscall.F_SETLKW, &syscall.Flock_t{
			Type:   int16(lt),
			Whence: io.SeekStart,
			Start:  0,
			Len:    0, // 所有字节。
		})
		if err != syscall.EINTR {
			return err
		}
	}
}

func isNotSupported(err error) bool {
	return err == syscall.ENOSYS || err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP || err == ErrNotSupported
}
